-
-
Notifications
You must be signed in to change notification settings - Fork 590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Debounce Timeout on XMLHTTPRequest if loading #330
Conversation
3a7f1e7
to
5b4cea6
Compare
* | ||
* @param {function} func callback to be called after a delay | ||
* @param {Number} waitMs number of milliseconds to delay by | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing docs for jobName
.
|
||
module.exports.debounce = function(jobName, func, waitMs) { | ||
if (jobName === undefined) { | ||
while(_debouncers[jobName = Math.random().toString(16).slice(2, 10)]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the while
? This seems rather opaque given you're just generating an ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to make sure it doesn't collide with an existing random ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aaah, right I see now. This line feels a bit too clever, you're:
- Generating the random ID.
- Assigning it to
jobName
. - Keying into
_debouncers
to check if there's a collision. - If there is a collision, rinse and repeat else continue to the next line.
...all on one line. Please can you break this up onto multiple lines which are more self-explanatory, and even add a comment to say what you're doing e.g:
// make sure the job name doesn't collide with an existing debouncer
while (!jobName || _debouncers[jobName]) {
jobName = Math.random().toString(16).slice(2, 10);
}
return jobName; | ||
}; | ||
/** | ||
* Clear a debounce job.. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extra .
* | ||
* @param {String} a jobName to reference the debouncer. | ||
* | ||
* @return {Bool} whether a debouncer with jobName was cleared. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{boolean}
for primitive booleans
please. I don't think JSDoc3 will like {Bool}
.
|
||
complete() { | ||
delete _debouncers[this.jobName]; | ||
this.func(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're assuming that a func
is always provided, even though you're null guarding on start(func, waitMs)
. Which is it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Supposing the first debounce call is like debounce('clientOffline', notifyClientOffline, 1000)
this would allow future debounce
calls to be made with just debounce('clientOffline')
without having to maintain a reference to the same function (but also to over-ride the callback of the debounce
if necessary).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, okay then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broadly speaking this is looking good. Given the importance of getting this right (it affects literally every HTTP request) I would like to see some tests for this please.
Tests are also failing on |
5b4cea6
to
e5a689a
Compare
// Jasmine expect(<method>).toThrow() seems to break self-references in | ||
// that method. | ||
httpBackend.flush().done(function() { | ||
done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing assertion on _startKeepAlives
? To check if it has/has not been called?
setTimeout(tryFlush, 0); | ||
// setImmediate(tryFlush); | ||
process.nextTick(tryFlush); | ||
// tick(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably you're doing this to get around the fact that we're now mocking time so setTimeout
isn't working as we expect. Can you add a comment to this effect please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, you do mention this further down! Wonderful! 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though one wonders why setImmediate
is used below and process.nextTick
is used here? Any reason for it? If not, can we be consistent please.
* | ||
* @param {Number} waitMs number of miliseconds of timeout. | ||
*/ | ||
timeout: function(waitMs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What this actually does is advance the amount of time this HTTP request has taken - this may or may not result in a timeout. Can you please clarify this in the docs, else it appears as if you're saying "time out the request after waitMs
".
@@ -869,6 +869,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) { | |||
* @return {promise} | |||
*/ | |||
SyncApi.prototype._startKeepAlives = function(delay) { | |||
console.log('KEEP ALIVES'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spurious console line.
3763439
to
b6200c3
Compare
this.requests = []; | ||
this.expectedRequests = []; | ||
this.useMockClock = useMockClock ? true : false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally mock-requests should always use a mock-clock but that seems to be break some internal setTimeout uses?
setTimeout(tryFlush, 5); | ||
triedWaiting = true; | ||
} else if (flushed === 0 && waitLoops < 50) { | ||
waitLoops += 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setTimeout
for waiting isn't compatible with mocking that out, so I ended up with something like this -- but it seems like a hack. I believe Jasmine has a waitFor (but it's removed in 2.0) it would be great to do this in a nicer way though...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You absolutely need to comment this and explain why you're doing this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like very much to improve this rather than committing it to the code-base (haven't done a lot of testing in Node), are you aware of a better strategy or a reference code-base to take a peek at?
callbacks.clearTimeout(timeoutId); | ||
if (timeoutContext) { | ||
timeoutContext.stop(); | ||
delete req.onprogres; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure whether clearing onprogress
on a request is required or not, once it has loaded or errored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well you're not spelling onprogress
correctly...
@@ -36,7 +36,7 @@ const DEBUG = true; | |||
// beyond that and wedge forever, so we need to track how long we are willing | |||
// to keep open the connection. This constant is *ADDED* to the timeout= value | |||
// to determine the max time we're willing to wait. | |||
const BUFFER_PERIOD_MS = 80 * 1000; | |||
const BUFFER_PERIOD_MS = 30 * 1000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decreased this since it is now debounced; I think 60 seconds to wait for an onProgressEvent is enough before timing out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep this at 80. We can tweak it further should it become a problem.
*/ | ||
|
||
module.exports.debounce = function(jobName, func, waitMs) { | ||
if (jobName === undefined || jobName === null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given jobName
is supposed to be a string
, why not just:
if (!jobName) {
// ...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess jobName could be 0
? If there is no reasonable case for that than !
should be fine.
|
||
start(func, waitMs) { | ||
if (func) this.func = func; | ||
if (!isNaN(waitMs)) this.waitMs = waitMs; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't do what you want it to do.
> isNaN(null)
false
Meaning waitMs
could be set to null
. You really only want positive waitMs
values, so just do:
if (waitMs > 0) {
this.waitMs = waitMs;
}
@@ -1,14 +1,26 @@ | |||
"use strict"; | |||
const q = require("q"); | |||
let clock = jasmine.Clock; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you aliasing jasmine.Clock
? It's useful to know at the callsites that you're manipulating jasmine's stuff and not our own, and the alias removes this context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was following the way it was done in realtime-callbacks.spec.js
, /agree referencing it explicitly is better.
@@ -679,6 +720,10 @@ module.exports.MatrixHttpApi.prototype = { | |||
handlerFn(err, response, body); | |||
} | |||
); | |||
if (timeoutContext) { | |||
req.onprogress = timeoutContext.onProgressCheck; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this work as intended without .bind()
ing, given you utilise this
in onProgressCheck
?
module.exports.__TimeoutContext = TimeoutContext; | ||
|
||
TimeoutContext.prototype.onProgressCheck = function(progressEvent) { | ||
if (this.lastLoaded || progressEvent.loaded > this.lastLoaded) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you comparing Numbers to null
initially? Why isn't lastLoaded
set to 0
? Also, you should comment why you are debouncing for any previous positive number. Isn't progressEvent.loaded > this.lastLoaded
good enough? One can make the argument that if the request has wedged at say 20582
loaded bytes, and isn't changing, then you should NOT be debouncing and waiting forever if we are not making any progress.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be if (!this.lastLoaded ||
or rather if (this.lastLoaded != null
. Is it safe to just set lastLoaded to 0, the first onProgressEvent may have loaded 0
or is that not the case?
I think I'm about to make myself unpopular. 😞 You only need to do a few actions to debounce requests - repeatedly refresh a This PR is adding 2 new classes: I appreciate the desire to make generic, reusuable components which may be of use elsewhere, but the fact is that they aren't being used elsewhere at the moment, and I feel are adding needless complexity. I wouldn't be so paranoid if it weren't the fact that this is literally touching every single HTTP request that will be made in both clients and bridges: I want to have more confidence in this PR and I hoped the tests would do that, but it isn't. Can you please re-write this as simply as possible: there is no shame to inlining the debouncing code in the EDIT: For an example of what I'm trying to guide you towards, is replacing this section in if (localTimeoutMs) {
timeoutId = callbacks.setTimeout(function () {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs
}));
}, localTimeoutMs);
} with something to the effect of: if (localTimeoutMs) {
let timedOut = function () {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs
}));
};
let timeoutId = setTimeout(timedOut, localTimeoutMs);
let loaded = 0;
req.onprogress = function(ev) {
if (ev.loaded > loaded) {
loaded = ev.loaded;
clearTimeout(timeoutId);
timeoutId = setTimeout(timedOut, localTimeoutMs);
}
}
} |
b80e207
to
2833021
Compare
You only need to do a few actions to debounce requests - repeatedly
refresh a setTimeout,
In order to repeatedly refresh a setTimeout two separate contexts are
required, the `timeoutId` of the previous timeout to clear and the `loaded`
number returned from the last ProgressEvent. Both of these need to be made
available to the `onprogress` function and both need to be stored in a
permanent data-structure since `loaded` and `timeoutId` will both change
per `onprogress` call. If you would prefer I can inline the data-structure
into the request creation (I think this is what you are suggesting?) but
from my estimation that would add *more* rather than *reduce* the amount of
untestable code.
In fact currently the `/sync` function is not tested anywhere directly (it
requires internal state and has no return value) --- all of the spec/integ
tests use `startClient()`: hence the initially hackish setTimeout(tryFlush,
5) in the mock-request.
…On Fri, Jan 20, 2017 at 2:14 PM, Kegsay ***@***.***> wrote:
I think I'm about to make myself unpopular. 😞
You only need to do a few actions to debounce requests - repeatedly
refresh a setTimeout, and yet this PR has got a *lot* of additional bloat
around such a simple concept.
This PR is adding 2 new classes: Debouncer and TimeoutContext, both of
which are internal to the SDK and both of which are used in precisely 1
place: to debounce HTTP requests. In order to expose enough information to
drive this in tests you're exporting additional "private" functions like
__TimeoutContext and __getDebouncers. You're modifying the HttpBackend to
add additional timeout logic which is used solely in said tests. All of
this adds what I feel is needless uncertainty to this PR.
I appreciate the desire to make generic, reusuable components which may be
of use elsewhere, but the fact is that they *aren't* being used elsewhere
at the moment, and I feel are adding needless complexity. I wouldn't be so
paranoid if it weren't the fact that this is literally touching every
single HTTP request that will be made in both clients and bridges: I want
to have more confidence in this PR and I hoped the tests would do that, but
it isn't.
Can you please re-write this as simply as possible: there is no shame to
inlining the debouncing code in the _request function in HttpApi where
appropriate if it improves clarity, which for such a simple task it really
will. I appreciate you've spent a decent amount of time on this by now, and
I'm sorry my bar is set quite so high, but I hope you can understand where
I'm coming from given how important this section of code is.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#330 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AJwz-KtMN9QvA90mcmdXe1vGCguYaBnAks5rUOtqgaJpZM4Lk1z6>
.
|
ff5e5be
to
73b6e95
Compare
I don't care whether or not we test whether the |
900b68c
to
f485519
Compare
Closing since #392 is merged |
Addresses the same issue as #327 but keeps the timeout in case of the browser failing to correctly error out a stagnant request per @kegsay's comment.